# Copyright 2010 Giacomo Alzetta
# This file is part of "LimberEditor" and is distribuhited under the
# terms of the MIT license.

'''
limber.componentchooser
=======================

This module contains the classes and functions used to manage
the abilities of the documents and views.
'''

import bisect


class ComponentChooserError(Exception):
    '''This error is raised by the *ComponentChooser*
    when the user try to execute invalid operations on it.
    '''

    pass


class ComponentChooser(object):
    '''Manages the abilities.'''

    def __init__(self):

        self._components = {}

    def register_component(self, component, abilities):
        '''Register a new component.'''

        self._components[component] = abilities

    def unregister_component(self, component):
        '''Unregister the given component.'''

        try:
            del self._components[component]
        except KeyError:
            try:
                name = component.__name__
            except AttributeError:
                name = component
            raise ComponentChooserError('There is no %s component managed.' % name)

    def number_of_components(self):
        '''Return the number of components registered.'''

        return len(self._components)

    def components(self):
        '''Return a sequence of the registered components.'''

        return tuple(self._components.iterkeys())

    def get_abilities(self, component):
        '''Return the abilities associated to the given component.'''

        try:
            return tuple(self._components[component])
        except KeyError:
            try:
                name = component.__name__
            except AttributeError:
                name = component
            raise ComponentChooserError('There is no %s component managed.' % name)

    def get_component(self, abilities, all_required=True):
        '''Get the best component for the given abilities.

        This method returns a tuple: component, abilities.

        If *all_required* is `True` then the method will
        either return a component that matches all the
        abilities or raise a *ComponentChooserError*.

        If *all_required* is `False` the component returned
        might not have all the abilities listed in *abilities*.
        If there isn't any component that have anyone
        of the listed abilities than *ComponentChooserError* is raised.
        '''

        abilities = set(abilities)
        best_component = None
        best_abilities = 0

        for component, comp_abilities in self._components.iteritems():
            got = abilities.intersection(comp_abilities)
            if len(got) > best_abilities:
                best_abilities = len(got)
                best_component = component

        if all_required and best_abilities < len(abilities) or best_component is None:
            raise ComponentChooserError('There isn\'t any component with all '
                                      'the required abilities.')
        elif best_component is None:
            raise ComponentChooserError('There isn\'t any component with the '
                                      'matching abilities.')
        return best_component, self._components[best_component]

    def get_components(self, abilities, all_required=True):
        '''Return the matching components for the given abilities.

        This method return a tuple: components, abilities.

        If *all_required* is `True` than each component only
        components that have all the abilities are considered.

        If *all_required* is `False` then all components that
        have at least an ability listed in *abilities* will
        be returned.

        If no matching component is found, an *ComponentChooserError*
        is raised.
        '''

        abilities = set(abilities)
        components = []
        components_abilities = []
        ab_numbers = []

        for component, comp_abilities in self._components.iteritems():
            got = abilities.intersection(comp_abilities)
            if all_required and len(got) == len(abilities):
                components.append(component)
                components_abilities.append(comp_abilities)
            elif got:
                number_of_abilities = len(got)
                index = bisect.bisect(ab_numbers, number_of_abilities)
                ab_numbers.insert(index, number_of_abilities)
                components.insert(index, component)
                components_abilities.insert(index, comp_abilities)

        if not components:
            raise ComponentChooserError('There isn\'t any component with the '
                                      'matching abilities.')

        return components[::-1], components_abilities[::-1]

    def fetch_component(self, required_abilities, other_abilities=(),
                              priorities=()):
        '''Get the component that matches the given options.

        It returns a tuple: component, abilities.

        Only components having all the *required_abilities*
        are considered.

        The *other_abilities* argument should be an iterable
        whoose abilities are optional.

        The *priorities* argument should be an iterable of
        integers that will be coupled with the corrispondent
        optional ability and will be used to define which
        component should be returned.

        If the *priorities* argument is shorter than the
        *other_abilities* argument, then priority 1
        is given to the elements missing.
        '''

        required_abilities = set(required_abilities)
        other_abilities = tuple(other_abilities)
        priorities = list(priorities)

        if len(priorities) < len(other_abilities):
            delta_length = len(other_abilities) - len(priorities)
            priorities.extend([1]*delta_length)

        best_component = None
        best_abilities = 0

        for component, comp_abilities in self._components.iteritems():
            intersection = required_abilities.intersection(comp_abilities)
            if len(intersection) < len(required_abilities):
                continue

            points = 0
            for other_ability, priority in zip(other_abilities, priorities):
                if other_ability in comp_abilities:
                    points += priority
            if points > best_abilities:
                best_component = component
                best_abilities = points

        if best_component is None:
            raise ComponentChooserError('There isn\'t any component with all '
                                      'the required abilities.')

        return best_component, self._components[best_component]

    def fetch_components(self, required_abilities, other_abilities=(),
                               priorities=()):
        '''Return the matching components for the given query.

        It returns a tuple: components, components_abilities.

        Only components having all the *required_abilities*
        are considered.

        The *other_abilities* argument should be an iterable
        whoose abilities are optional.

        The *priorities* argument should be an iterable of
        integers that will be coupled with the corrispondent
        optional ability and will be used to define
        the score of the components, and thus the order
        of the sequence returned.

        If the *priorities* argument is shorter than the
        *other_abilities* argument, then priority 1
        is assigned to the elements missing.
        '''

        required_abilities = set(required_abilities)
        other_abilities = tuple(other_abilities)
        priorities = list(priorities)

        if len(priorities) < len(other_abilities):
            delta_length = len(other_abilities) - len(priorities)
            priorities.extend([1]*delta_length)

        components = []
        components_abilities = []
        ab_numbers = []

        for component, comp_abilities in self._components.iteritems():
            intersection = required_abilities.intersection(comp_abilities)
            if len(intersection) < len(required_abilities):
                continue

            points = 0
            for other_ability, priority in zip(other_abilities, priorities):
                if other_ability in comp_abilities:
                    points += priority

            index = bisect.bisect(ab_numbers, points)
            ab_numbers.insert(index, points)
            components.insert(index, component)
            components_abilities.insert(index, comp_abilities)

        if not components:
            raise ComponentChooserError('There isn\'t any component with '
                                      'all the required abilities.')

        return components[::-1], components_abilities[::-1]